之前有写过单例模式:[设计模式]之三:单例模式,可以通过链接回顾一下。然后这次再多聊聊这个设计模式。
线程不安全的懒汉式
可能一提到单例模式,大多数人都会想到这么写:
public class Singleton {
private static Singleton instance;
private Singleton() {
}
public static Singleton getInstance(){
if (null == instance) {
instance = new Singleton();
}
return instance;
}
}
这个写法也是严格按照概念来的
让类自身负责保存它的唯一实例。这个类保证没有其它实例可以被创建,并且它可以提供一个访问该实例的方法。
但这个写法有一个问题:多线程情况下并行调用Singleton.getInstance()
,会导致创建多个实例。
可以用代码做个简单验证:
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("s1 hashcode:" + Singleton.getInstance().hashCode());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println("s2 hashcode:" + Singleton.getInstance().hashCode());
}
}).start();
通过hashCode
看到有时候获取到的两个实例并不相同。
线程安全的懒汉式
对于多线程情况,一个直接的办法就是使用同步锁synchronized
。
public static synchronized Singleton getInstance(){
if (null == instance) {
instance = new Singleton();
}
return instance;
}
再次执行测试程序,现在不论怎么执行,打印的哈希值都是一样的。这样很轻松地解决了同步问题。
BUG解掉了不代表工作就结束了,我们还没看程序性能呢。处理性能问题,首先要梳理程序逻辑。
同步锁解决的是什么问题?
是会创建多个实例。
如果保证了只有一个实例,后面get实例还有问题吗?
没有
那么问题就出来了,同步锁保证同时只有一个线程访问它,这个功能只在实例未创建的时候需要。其他时间,我们认为它可以随便调用。否则的话多线程运行到这里就成单线程了,一个一个执行,太影响效率了。
所以就需要先做一个判断,如果实例未创建,加锁!否则就返回实例。这样就形成了双重检验。
双重检验锁
public static Singleton getInstance(){
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
首先,检查实例是否存在,不存在就进行加锁。但是加锁后,里面为何又要判断实例为空呢?因为可能有多线程一起通过第一层,而到了同步锁这里,几个线程排队执行。这样第一个线程创建实例后,第二个线程就会进来。所以需要再做一次判断,防止多创建实例。
到此,一个近乎完美的单例就写出来了。说是近乎,是因为还有一个很细小问题没有解决:指令顺序优化。
instance = new Singleton();
这行代码,在JVM中执行:(描述可能不是很准确,后续再修改)
- 为instance分配内存
- 调用Singleton构造函数初始化成员变量
- 将创建的对象指向分配的内存空间
执行了1,2,3后,instance才是非空的。但是在JVM编译时,为了提高执行效率,这些指令执行顺序会被调整。如果出现1,3,2的执行顺序,那么程序就会报错。
解决方案就是使用volatile
关键字禁止指令排序优化。
private volatile static Singleton instance;
关于volatile
关键字,是涉及到JVM书籍里都会讲到的。我会另外去总结,这里不多BB了。
饿汉式
前面的都是在第一次使用的时候,初始化实例,所以叫做懒汉式。那么饿汉式就是在使用前直接创建实例给你用。
private static final Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
其实这样更简单呢。
因为static final在第一次加载到内存就执行了,所以不用担心线程问题。
但是毕竟这样还是有缺陷的,就是如果涉及到根据参数初始化的情况,这个写法就不奏效了。
静态内部类
这个是
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
后面也会再聊聊内部静态类这个东西。
枚举单例
private static enum EnumSingleton {
INSTANCE;
private Singleton singleton;
private EnumSingleton() {
singleton = new Singleton();
}
private Singleton getInstance() {
return singleton;
}
}
private Singleton() {
}
public static Singleton getInstance(){
return EnumSingleton.INSTANCE.getInstance();
}
目前最为安全的实现单例的方法是通过内部静态enum的方法来实现,因为JVM会保证enum不能被反射并且构造器方法只执行一次。
枚举类型可以干很多有意思的事情,后面也会拿出来聊一聊。